package linker

import (
	"bytes"
	"debug/elf"
	"risc-v/ld/pkg/utils"
	"strconv"
	"strings"
	"unsafe"
)

const PageSize = 4096

// IMAGE_BASE 基地址
const IMAGE_BASE uint64 = 0x200000
const EF_RISCV_RVC uint32 = 1

// FileHeaderSize the size of FileHeader struct
const FileHeaderSize = uint(unsafe.Sizeof(FileHeader{}))

// SectionHeaderSize the size of SectionHeader struct
const SectionHeaderSize = int(unsafe.Sizeof(SectionHeader{}))
const SymbolEntrySize = int(unsafe.Sizeof(ELFSymbol{}))

const ArchiveSectionHeaderSize = int(unsafe.Sizeof(ArchiveSectionHeader{}))

// ProgramHeaderSize is the size of ProgramHeaderSize
const ProgramHeaderSize = int(unsafe.Sizeof(ProgramHeader{}))

const RelaSize = int(unsafe.Sizeof(Rela{}))

// FileHeader representation the relevant information of elf
// more detail: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
// ELF 文件头
type FileHeader struct {
	Ident       [16]uint8 // identify the elf basic information,such as type,ABI, byte order,32-bits or 64-bits format
	Type        uint16    // relocatable file, shared object or executable file etc.
	Machine     uint16    // specifies the instruction set architecture,such as aarch64(arm64), x86_64(amd64)
	Version     uint32    // set to 1 for the original version of elf
	Entry       uint64    // memory address of entry point form where the process start execuating.
	Phoff       uint64    // the offset of program header
	Shoff       uint64    // the offset of section header
	Flags       uint32    // interpretation of this field depends on the target architecture
	Ehsize      uint16    // the size of this elf header,64 bytes for 64-bits and 52 bytes for 32-bits
	PhEntrySize uint16    // the size of program header table entry
	PhNum       uint16    //the number of entries in program header table
	ShEntrySize uint16    // the size of section table entry
	ShNum       uint16    // the num of entries in section header table
	ShStrIndex  uint16    //more details: https://paper.seebug.org/89/
	// section header table 中字符串表的索引
}

// ProgramHeader 程序头表基本组成单元
// 程序头表是一个数组，数组中的每一个元素称为程序头
// 每一个程序头描述了一个段(segment)
// 一个目标文件中的段(segment) 包含一个或者多个 section
// 程序头只对可执行文件或者共享目标文件(动态库) 有意义
type ProgramHeader struct {
	Type     uint32 // 段的类型，加载器将决定如何解析本程序头
	Flags    uint32 // 本段的内容属性
	Offset   uint64 // 本段内容在文件中的偏移(位置)
	VAddr    uint64 // 本段内容的开始位置在进程空间的虚拟地址
	PAddr    uint64 // 本段内容的开始位置相对于文件开头的偏移量,一般不使用
	FileSize uint64 // 本段内容在文件中的大小
	MemSize  uint64 // 本段内容在内存镜像中的大小, FileSize <= MemSize 差别主要在 .bss 节
	Align    uint64 // 对齐值,要求 VAddr 和 Offset 在取模后应该相等
}

// SectionHeader is the section header entry
// 节头部表是一个数组, 包含多个 SectionHeader
type SectionHeader struct {
	Name      uint32 // section 的名字 实际上是在 .shstrtab 中的偏移量
	Type      uint32 // section 的类型 program data, symbol data 等等
	Flags     uint64 //标识 section 的属性
	Addr      uint64 //section 在内存中的虚拟地址，section 应该是 load 类型的
	Offset    uint64 //section 相对于文件头的偏移量
	Size      uint64 //section 占用的内存大小，以字节为单位
	Link      uint32 //和当前 section 相关 section 的索引
	Info      uint32 //包含当前节的额外的索引
	AddrAlign uint64 //当前节内存对齐的字节数，此字段必须是2的整数幂
	EntrySize uint64 //对于固定节来说，表示整个条目的大小
}

// ELFSymbol ELF中 .symtab 中的项，比如全局变量，函数等
// 使用 readelf -s [ELF file] 查看
// more detail https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html
type ELFSymbol struct {
	Name  uint32 // 符号的名字, 如果非零实际则是 .strtab 中的索引,否则此符号没有名字
	Info  uint8  // 指定符号类型和其绑定的属性 如 STB_LOCAL:0 STB_GLOBAL:1
	Other uint8  // 指定符号的可见性
	Shndx uint16 // 符号相关联的节，即 section header table 中的index,如果是特殊值 SHN_XINDEX , 则Shndx 发生溢出
	Value uint64 // 和符号的类型有关, 详情参见 doc 文档
	Size  uint64 // 符号实际占用的字节数
}

// IsAbs 判断 ELFSymbol.Shndx 的值是否是 SHN_ABS
// 如果是, 表明符号的值是绝对的,具有常量性,在重定位过程中 Val 字段 不需要改变
func (s *ELFSymbol) IsAbs() bool {
	return s.Shndx == uint16(elf.SHN_ABS)
}

// IsUndef 判断 ELFSymbol.Shndx 的值是否是 SHN_UNDEF
// 如果是, 表明本符号在当前目标文件中未定义，链接器会找到此符号定义的文件，并把这些文件链接一起
func (s *ELFSymbol) IsUndef() bool {
	return s.Shndx == uint16(elf.SHN_UNDEF)
}

// IsCommon 本符号关联的是一个还没有被分配的节,本符号的值规定了内内容的字节对齐规则
// 链接器需要为本符号分配存储空间 而且其起始地址是向 Value 对齐的
// 也就是说,此符号的值指明了这个节内容的字节对齐数
func (s *ELFSymbol) IsCommon() bool {
	return s.Shndx == uint16(elf.SHN_COMMON)
}

// ArchiveSectionHeader
// more detail:https://archive.ph/20161010051456/http://www.iecc.com/linker/linker06.html
// 具体的 静态库文件格式请参考具体 doc 文档
type ArchiveSectionHeader struct {
	Name    [16]byte // member name
	ModTime [12]byte // modification time
	Uid     [6]byte  // user id
	Gid     [6]byte  // user group id
	Mode    [8]byte  // octal file mode
	Size    [10]byte // member size  in bytes
	Eol     [2]byte  // reverse quote, newline
}

// Rela 重定位项结构
// 一个重定向节包含多个此项
type Rela struct {
	Offset uint64 // 受重定位作用的存储单元在节( Section )中的字节偏移
	Type   uint32 // 指定重定位项如何修改对应的位域
	Symbol uint32 // 重定位节所作用的符号表索引 通常是 Info
	Addend int64  // 被用来计算存储在重定位域的值
}

// GetSectionEntryName 通过 .strtab 和对应的 offset 获得对应的字符串名字
//
//	strTab := ['0','.','t','e','x','t','0','.','b']
//	offset := 1
//	res:=GetSectionEntryName(strTab,offset)
//	println(res) // .text
func GetSectionEntryName(strTab []byte, offset uint32) string {
	length := bytes.Index(strTab[offset:], []byte{0})
	return string(strTab[offset : offset+uint32(length)])
}

// HasPrefix 判断 ArchiveSectionHeader.Name 是否含有指定的前缀
func (ar *ArchiveSectionHeader) HasPrefix(prefix string) bool {
	return strings.HasPrefix(string(ar.Name[:]), prefix)
}

// IsStrTab 判断静态文件中的节(非ELF中的Section)类型
// 一个静态文件 只有一个 字符串表
// 用来保存各个 .o 文件的文件名
func (ar *ArchiveSectionHeader) IsStrTab() bool {
	return ar.HasPrefix("// ") // 后面是有空格的
}

// IsSymTab 判断是否是符号表(非 ELF 中的符号表)
// 一个 .a 文件只有一个符号表
func (ar *ArchiveSectionHeader) IsSymTab() bool {
	return ar.HasPrefix("/ ") || ar.HasPrefix("/SYM64/ ") //后面是有空格的
}

func (ar *ArchiveSectionHeader) SizeToInt() int {
	size, err := strconv.Atoi(strings.TrimSpace(string(ar.Size[:])))
	utils.MustNo(err)

	return size
}

// ReadName 读取这一块所属于的 object 文件的文件名 文件名字符数小于等于 16的直接从 Name 字段读出 长于 16
// 个字符的从字符串表中读出 COFF, ELF and Windows archives store names longer than 16
// characters in an archive member called //.
func (ar *ArchiveSectionHeader) ReadName(strTab []byte) string {
	// long filename, beyond 16 characters
	if ar.HasPrefix("/") {
		index, err := strconv.Atoi(strings.TrimSpace(string(ar.Name[1:])))
		utils.MustNo(err)
		length := bytes.Index(strTab[index:], []byte{'/', '\n'})
		if length == -1 {
			utils.Fatal("archive file format error")
		}

		return string(strTab[index : index+length])
	}

	// short filename
	end := bytes.Index(ar.Name[:], []byte{'/'})
	utils.Assert(end != -1)

	return string(ar.Name[:end])
}
